feat(goal): let the model propose a Goal the user approves in a dialog - #10171
Conversation
Adds a propose_goal core tool: the model proposes a session Goal, the user sees the full objective in an approval dialog, and only their approval sets it, exactly as /goal set would. Declining sets nothing and the model is told only that the Goal was not set. The tool is registered beside get_goal/update_goal (never for subagents), only where a dialog can be shown (interactive, ACP, stream-json; never plain headless) and only while goals.modelProposed is not "disabled". The dialog reuses the generic info confirmation, so no host needs a new component: the objective is in the invocation description (the one field every host forwards) and in the plain-text prompt with a one-sentence explanation. requiresUserInteraction() is true, so no allow rule, skill grant, YOLO, or AUTO_EDIT (which auto-approves info confirmations) can skip the dialog. Preconditions are checked before the dialog and again in execute(), because /goal can change the session while it is open: plan mode, an untrusted folder, no Goal persistence, and an active Goal refuse with guidance. An active Goal is never replaced from the tool; a stopped Goal is replaced through `replace` with its expected version. The tool only dispatches: the runtime's broadcast renders the card and starts the first Goal turn after the current turn ends. goals.modelProposed (alwaysAsk | disabled) joins WORKSPACE_RESTRICTED_SETTINGS so a repository cannot switch the tool on; the CLI normalizes it to the closed enum. The goal-draft skill hands off through propose_goal when it is available and no Goal is active, and keeps its printed /goal set line for headless runs, the disabled setting, and the active-Goal case. Follow-up to QwenLM#10002 (P2 of the goal-draft design note). Claude-Session: https://claude.ai/code/session_01FV7i3w7egJ2kMw4AhQC38Z
|
|
… columns The first cut used a quoted default that widened the Default column, so prettier re-aligned 46 unrelated rows. The row now fits the existing widths and the table diff is the one added line. Claude-Session: https://claude.ai/code/session_01FV7i3w7egJ2kMw4AhQC38Z
|
Thanks for the PR! Template looks good ✓ Problem: real and observed. #10002 shipped Direction: aligned. This is the planned P2 of the goal-draft design note, and Claude Code's Size: core paths touched ( Approach: scope feels right — it matches what the design doc scoped. Reusing the generic Risk: no elevated risk signals — none of the changed files match the revert-correlated high-risk paths. Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题:真实存在。#10002 交付的 方向:对齐。这是 goal-draft 设计说明里规划好的 P2;Claude Code 的 规模:触及核心路径( 方案:范围合理,与设计文档圈定的范围一致。复用通用 风险:无升级风险信号——改动文件均未命中与 revert 相关的高风险路径。 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewThe approach matches what I'd have proposed independently — new tool beside
One non-blocking nit: sequenceDiagram
participant P1 as Model
participant P2 as Tool scheduler
participant P3 as ProposeGoalInvocation
participant P4 as User dialog
participant P5 as Goal runtime
P1->>P2: calls propose_goal with objective
P2->>P3: permission flow forces ask and hides always-allow
P3->>P3: checks plan mode, trust, persistence, active Goal
P3->>P4: info dialog with the full objective
P4-->>P3: approve or decline
P3->>P5: dispatch create or versioned replace
P5-->>P3: Goal record or conflict error
P3-->>P1: result telling it to acknowledge and end the turn
P5->>P5: broadcast renders Goal card and starts first Goal turn
Files changed (19 of 19 shown)
Testing evidence (the PR's own CI — per policy this review does not run PR code)The ubuntu unit suite and the web-shell visual capture are still running as of this review; the windows/macos unit jobs and the CLI integration job were skipped by workflow conditions (not failures). The Desktop Shell jobs on both platforms passed — those compile the whole monorepo including this diff, which is a real build signal. No red checks on this commit at review time. Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 The remaining checks on the commit are bot orchestration (review-pr, label, authorize, …). Not verified: live TUI/Web Shell/ACP rendering of the dialog — the author's tmux capture below is their evidence, not an independent re-run:
中文说明代码审查方案与我独立给出的设想一致——在
一个非阻塞小项: 两处既有的 goal 工具枚举没有加入 测试证据(引自 PR 自身的 CI——按策略本审查不运行 PR 代码)截至审查时 ubuntu 单测与 web-shell 视觉截图仍在运行;windows/macos 单测与 CLI 集成测试被工作流条件跳过(不是失败)。两平台的 Desktop Shell 任务已通过——它们连同本 diff 编译整个 monorepo,是真实的构建信号。审查时该提交无红色检查。上方表格区域会由 finalize 流程在 CI 落定后就地更新。 未验证:TUI/Web Shell/ACP 对话框的实际渲染——作者在 Linux 上的 tmux 记录是其自述证据,不是独立复跑。维护者可触发 — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — clean, minimal, direction pre-approved in the merged design doc; the only open items are the non-blocking enumeration nit and CI still in flight. Stepping back: this is exactly the follow-up phase the goal-draft design note promised, and it lands the hard parts right. My independent proposal for this feature and the PR's implementation converge on the same shape, and where the PR goes further — the versioned Reservations, plainly stated: the cross-host rendering of the dialog (TUI buttons, Web Shell description-only approval, ACP) is covered by unit tests and the author's tmux session but not independently exercised here, and the ubuntu unit suite had not finished when this review ran. Neither is a reason to hold the PR — they are what the green-CI precondition and the sandboxed lanes above are for. Verdict: approve, with approval deferred until CI lands green on 中文说明置信度:4/5 —— 干净、最小化,方向在已合并的设计文档中已预先确认;唯一未决项是上面那个非阻塞枚举小项,以及 CI 仍在运行。 退一步看:这正是 goal-draft 设计说明承诺的后续阶段,而且难点都做对了。我独立给出的方案与 PR 的实现收敛到同一形状;PR 更进一步的地方——带版本的 坦率地说保留意见:对话框的跨宿主渲染(TUI 按钮、Web Shell 仅描述批准、ACP)由单测和作者的 tmux 记录覆盖,但这里没有独立复跑;审查时 ubuntu 单测尚未结束。这两点都不是扣住 PR 的理由——绿色 CI 前置条件和上面的沙箱通道正是为此而设。 结论:批准,但批准延迟到 CI 在 — Qwen Code · qwen3.8-max Reviewed at |
🖼️ web-shell visual previewRendered against a mock daemon (no real backend): the PR base vs this PR head Screenshots · before / afterFull-resolution recordings (.webm) are attached to the workflow run. — Qwen Code · web-shell visuals |
|
已修复 |
… turn ends The first end-to-end run set the Goal from inside propose_goal's execute(). That works for the Goal but not for the turn that proposed it: with a Goal now active, the tool-result continuation of that turn has no Goal permit and client.ts rejects it with "An active Goal requires an exact turn permit", which surfaces as an error card before the runtime's own Goal turn takes over. The tool now parks the approval on Config (setPendingGoalProposal) and the client applies it at the boundary a typed /goal set takes effect: once the model has stopped calling tools, before Stop hooks (settlePendingGoalProposal -> applyPendingGoalProposal). The runtime's broadcast then renders the Goal card and starts the first Goal turn right after the model's acknowledgement. An approval still parked when the next real user query starts belongs to a cancelled turn and is discarded; a Goal that became active meanwhile is never replaced, and a moved expected version is reported as a conflict instead of thrown. Claude-Session: https://claude.ai/code/session_01FV7i3w7egJ2kMw4AhQC38Z
…t/propose-goal-tool
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — the 'Integration Tests (CLI, No Sandbox)' check was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): "agent reverse-audit (round 5)": none — the walk completed inside budget; no check was cut short..
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — the 'Integration Tests (CLI, No Sandbox)' check was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 5)":none — the walk completed inside budget; no check was cut short.。
— qwen3.8-max via Qwen Code /review (v0.22.2)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
5 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- R1-7 blocker() readiness gate — still stands (code unchanged), already reported (comment 3869318070)
- R1-8 last-write-wins parking slot — still stands (code unchanged), already reported (comment 3869318076)
- R1-9 approvedAt write-only field — still stands (code unchanged), already reported (comment 3869318082)
- R1-10 replacedGoalId unread field — still stands narrowed to replacedGoalId (the goal field is now read by the pause rollback), already reported (comment 3869318103)
- R1-11 five untested propose_goal lifecycle behaviors — all 5 locations still stand (cli wiring, entry abort branch, park gate, runtime-replacement clear, execute re-check), already reported (comments 3869318110, 3869318115, 3869318123, 3869…
Not reviewed: build-and-test — the 'Integration Tests (CLI, No Sandbox)' check was skipped in CI and its suite did not run locally.
Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.
Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:
packages/core/src/config/config.ts:7459 — [review] Config park/take pair never exercised by any test; the take-as-peek mutation (removing the clear inside takePendingGoalProposal) ships the scoped suite 663/663 green — anchored on code unch…
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
本轮确认的 5 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:build-and-test — the 'Integration Tests (CLI, No Sandbox)' check was skipped in CI and its suite did not run locally。
未审查:反向审计——在 5 轮的反审轮数上限内未收敛。
收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.2)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
Not reviewed: build-and-test — the 'Integration Tests (CLI, No Sandbox)' check was skipped in CI and its suite did not run locally.
Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round:
packages/core/src/goals/goal-tools.test.ts:1878 — [probe] applyPendingGoalProposal's persistence-failure catch branch has no test
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
未审查:build-and-test — the 'Integration Tests (CLI, No Sandbox)' check was skipped in CI and its suite did not run locally。
收敛姿态下延后(第 3 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.2)
… uncovered Adds the tests round 3 asked for, each verified to go red under the mutation it guards against: - goal-tools: execute() refuses when the set-once parking slot rejects a second approval that passed the pending re-check (deleting the branch fails it). - client-goal: "cancellation during runtime readiness" now asserts the proposal was taken and the slot is empty, not only that nothing was dispatched. - core config: propose_goal stays unregistered in interactive+stream-json and interactive+Zed sessions (isInteractive() true, mode 'acp'), so the gate cannot silently regress to isInteractive(); startNewSession clears a parked approval; take hands it out exactly once. - cli config: loadCliConfig wires goals.modelProposed through (default alwaysAsk, disabled, unknown value), red when the wiring line is dropped. goals.md no longer claims Web Shell has the tool; Web Shell and other ACP-driven sessions keep the printed /goal set hand-off until they have a turn-boundary settlement path. Also fixes the LlmEventType rename that merging main introduced into client-goal.test.ts.
|
Merged
Verification at 中文说明已合并
|
|
@qwen-code /takeover |
|
🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the 中文说明🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:
- applyPendingGoalProposal's GoalPersistenceUnavailableError catch clause has no test — already recorded as the round-3 deferral (review 5042138776)
Not reviewed: build-and-test — the 'Integration Tests (CLI, No Sandbox)' check was skipped in CI and its suite did not run locally.
Deferred under the convergence posture (round 4, not a blocker) — recorded, not requested in this round:
packages/core/src/core/client-goal.test.ts:778 — [probe] D4-1 next-query discard test cannot detect removal of client.ts:3137 (cap-path finally consumes the proposal)packages/core/src/core/client.ts:848 — [probe] D4-2 approved proposal that cannot be applied at turn end is dropped with a debug log only; user and model never toldpackages/core/src/core/client.ts:848 — [probe] D4-3 settle's !runtime guard has no crossing test (tsc catches a naive removal, but graceful-drop semantics are unpinned)
Convergence: round 4 posted 6 inline comment(s), 6 of them reported for the first time; the previous round posted 3 (3 new). Findings keep coming back to the same files: packages/core/src/core/client-goal.test.ts (findings in round 3; 1 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. (Observation only — nothing was withheld from this review because of this observation.)
中文说明
仅完成部分审查,审查缺口已披露。
本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。
未审查:build-and-test — the 'Integration Tests (CLI, No Sandbox)' check was skipped in CI and its suite did not run locally。
收敛姿态下延后(第 4 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。
收敛情况:第 4 轮发布了 6 条行内评论,其中 6 条是首次提出;上一轮发布了 3 条(其中 3 条首次提出)。发现反复回到同一批文件:packages/core/src/core/client-goal.test.ts(第 3 轮已出过发现,本轮又有 1 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,或将本 PR 的评审降到 --severity-floor critical,可以避免循环反复推导同一组发现。(仅为观察——本轮评审未因此扣留任何内容。)
— qwen3.8-max via Qwen Code /review (v0.22.3)
|
🤖 AutoFix hit a verification-gate error before reaching a verdict (attempt 11/100) — it will retry on the next scan.
Base conflict resolution (
|
|
@qwen-code /resolve |
|
🤖 AutoFix hit a verification-gate error before reaching a verdict (attempt 12/100) — it will retry on the next scan. Round summary — PR #10171 (Critical-only window, conflict round)Critical-only mode is active (window seeded at round 9, round counter ≥ 5). This round: merged Base-conflict resolution (required,
|
|
Qwen Code attempted to resolve merge conflicts but the run did not complete successfully. Check the workflow run for full logs. |
|
@qwen-code /resolve |
|
🤖 AutoFix ran out of time before finishing (timeout (3600000ms)) (attempt 13/100) — it will retry on the next scan.
See the Qwen Autofix agent step logs for model/tool output. 中文说明🤖 AutoFix 在完成前耗尽了时间(timeout (3600000ms))(第 13/100 次尝试)—— 将在下次扫描时重试。 Run log: https://github.com/QwenLM/qwen-code/actions/runs/33452214748 🧠 Handled by Qwen Code · model/模型 |
|
Qwen Code resolved the merge conflicts and pushed the branch update. The head branch moved to Merge resolution — PR #10171 ← upstream/mainRoot cause. Main's #10479 ( Textual, not semantic. The sides modified no shared row: main re-padded/edited existing rows, the PR only added one. Everything outside the table auto-merged. Resolution = keep main's table intact, re-insert the PR's row at the author's chosen position, re-padded to the new column widths: (abbreviated; the real row is space-padded to 1823 chars like every other table row) What is load-bearing.
Could not verify. No build/typecheck/tests run (out of scope). Only the conflicted file was modified. Cross-checks done: all 8 files referencing 中文说明根本原因。 main 分支的 #10479( 纯文本冲突,非语义冲突。 双方没有修改同一行。解决方式:完整保留 main 的表格,把 PR 新增的行按作者选定的位置( 关键约束。
未能验证的部分。 未运行构建或测试(超出本任务范围),仅修改了冲突文件。已核对:所有引用 |
qwen-code-dev-bot
left a comment
There was a problem hiding this comment.
Reviewed at head b69c986c.
- The two later commits after the round-9 review are upstream merges only; the goal files (
goal-tools.ts,goal-runtime.ts,client.tsproposal paths) are unchanged since the last findings round. 40/40 threads are resolved. - The round-8 Criticals were both recorded by the reviewer itself as deferred non-blockers (fails-closed, new surface). Re-checked against the code at this head: the turn-key binding drops the proposal when a new user/retry chain starts or a turn exits abnormally, only the owning
prompt_idframe may apply, and dispatch failure yields aapplied:falsereason rather than an active-but-unannounced Goal. The plan-mode / untrusted / active-goal preconditions are checked before the dialog and again inexecute()before parking. - Consent posture looks right:
requiresUserInteraction()blocks rule/YOLO/AUTO_EDIT auto-approval,goals.modelProposedis workspace-restricted so a repo cannot enable it, and headless/non-interactive sessions never register the tool. - CI on this head has no failures (
Test/Integration/security checks green; the review pipeline is still running); per the channel convention CI is outside my gate and this call is on the review itself.
yiliang114
left a comment
There was a problem hiding this comment.
Review passed.
- Consent gate is airtight:
requiresUserInteraction() = true+ default permissionask, and the scheduler honors that flag at every allow-rule / AUTO path, so no allow rule, YOLO, or AUTO_EDIT can skip the dialog. Preconditions are checked before the dialog and re-checked inexecute(). - The parked-approval lifecycle is handled exhaustively in
client.ts: settlement only at the proposing turn's true end (matched byprompt_id, kept parked through tool-result and tail-hook continuations), drop on abort/new user query/retry, discard on non-normal completion in the finalizer, and pause-if-aborted-during-dispatch.Config.startNewSessionclears a stale approval. - Registration is correctly gated: interactive sessions only; not registered under ACP, stream-json, headless, or
goals.modelProposed: disabled; subagent cores are non-interactive by construction. goals.modelProposedis workspace-restricted (repo cannot enable the tool), unknown values normalize to the default, and the vscode schema / i18n / web-shell display names are all updated.- Verified locally: goal-tools + client-goal + goal-draft skill tests 111/111; cli settings/config modelProposed tests pass; core Config registration/parking tests pass.
- CI: ubuntu Test failure is a runner-overload timeout cascade in files this PR does not touch, ending in a job-level cancel; web-shell E2E Smoke is a known main-level flake.
5c640ce
yiliang114
left a comment
There was a problem hiding this comment.
Re-review after the new push: the only changes since my previous approval are upstream/main merges (which now include the already-merged #10283); the feature diff is unchanged and the settings schema merge correctly keeps both goals.modelProposed and general.outputStyle. Previous verification still applies:
- Consent gate:
requiresUserInteraction+ defaultask, no allow rule / YOLO / AUTO_EDIT can skip the dialog; preconditions re-checked inexecute(). - Parked approval lifecycle covered at every client.ts exit path, bound to the proposing turn's prompt_id.
- Registration gated to interactive sessions only; workspace-restricted setting; i18n / web-shell / vscode schema updated.
- Local tests passed on the reviewed head: goal-tools + client-goal + goal-draft skill 111/111, cli config/settings modelProposed tests, core Config registration/parking tests.
qwen-code-dev-bot
left a comment
There was a problem hiding this comment.
Re-reviewed at head 5c640ce0 (my earlier approval on b69c986c was auto-dismissed by the two later upstream merges).
- Verified the increment is merge-only: the two commits are upstream syncs; the propose_goal files are untouched, and the drift on shared files (
config.ts,settingsSchema.ts,client.ts) comes from already-merged upstream PRs. The turn-settlement logic still reads intact at this head (13settle/takePendingGoalProposalsites), and 40/40 threads remain resolved. - CI on this head has no failures so far (Test and the review pipeline still running); per the channel convention the call is on the review itself.
- Note: the repo's review decision may still display CHANGES_REQUESTED from the older bot round whose deferred items were all test/doc follow-ups; those were re-checked at
b69c986cand are unchanged here.
…AME (QwenLM#10671) * fix(autofix): clamp gate test load explicitly instead of via RUNNER_NAME The verification gate launches through an env -i allowlist that drops RUNNER_NAME, so the vitest configs' ECS load clamps (60s test/hook timeouts, maxWorkers 25%) silently deactivate inside the gate: tests run with 15s timeouts, unbounded workers and coverage collection on a host shared with other autofix jobs. Under pool saturation this produced both false rejections (QwenLM#10171 round 3: 73 load-induced 15s timeouts in files the PR never touched, charged to the round) and gate deaths past the step's 60-minute cap that discarded verified fixes ("verification-gate error": QwenLM#10171 rounds 1/2/5-7, QwenLM#10543 five in a row). Pass the clamp values explicitly on both gate vitest invocations (the per-package --changed run and the bite check) so the verdict does not depend on env plumbing or runner naming, and disable coverage: nothing in the gate consumes it, and its collection dominated the overrun (72,000 CPU-seconds of collect in one 1,560s gate leg). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AhZA7LdQXZjcjfiPsoZkqZ * fix(autofix): clamp the gate's third vitest leg and pin the clamps Addresses review round 1 on QwenLM#10671. R1-6 The contracts check runs a web-shell vitest from inside the gate's own `env -i` child, and web-shell's config sets no timeouts at all, so that leg ran at vitest's 5s default on the same saturating host — the false-rejection class this PR removes, surviving in a sibling path. The clamp array moves above the contracts call and is handed to the shared script through AUTOFIX_VITEST_FLAGS; the issue-fix gate invokes the same script where RUNNER_NAME is present and leaves the variable unset, so its invocation is unchanged. R1-3 Nothing pinned that the clamps reach any invocation — every existing assertion is a prefix that ends before the expansion, so dropping it from a leg (or emptying the array, silent without `set -u`) stayed green while the gate reverted to 15s timeouts and coverage on. Three structural pins added on the review runner only, plus a contracts-script case that runs with AUTOFIX_VITEST_FLAGS set and asserts the flags reach npm. R1-4 The array hand-copies the ECS branch of three vitest configs, and inside the gate the CLI flags outrank the config — so raising an ECS ceiling to shelter a heavier test would leave the gate enforcing the old one and rejecting a fix that is green in normal CI. A parity test in scripts/tests/unit-vitest-configs.test.ts re-imports core, cli and acp-bridge under a stubbed ecs-qwen RUNNER_NAME (they read the env at import time) and asserts equality with the array parsed out of the shell script. R1-5 Narrowed the comment's claim, per the finding's own minimum. The residual is real and now named in the script: a handful of test files set their ceiling with a runtime `vi.setConfig` keyed on RUNNER_NAME, which outranks the CLI, so they keep their non-ECS values in here. Closing it needs a gate sentinel on both env -i allowlists plus a change in each file — a separate slice, not folded into this one. R1-1 is declined; see the thread. Its two premises did not reproduce against the lockfile-pinned vitest 1.6.1 under packages/sdk-typescript: the full suite passes with --maxWorkers=25% (37 files, 1747 tests, exit 0), and --maxThreads is rejected by 1.6.1 and 3.2.7 alike, so --maxWorkers is the spelling both majors accept rather than neither. * fix(autofix): pin the clamp witnesses and correct the unclamped-leg record Addresses review round 2 on QwenLM#10671; re-verifies round 1's fixes with mutation probes. R2-1 The comments justifying the unclamped issue-fix leg rested on a premise that does not hold for that leg: web-shell's vitest config sets no timeouts and has no RUNNER_NAME branch, so the drift test runs at vitest's 5s default wherever it runs. Corrected at all four mirror sites (both scripts and both test comments): the review gate passes explicit clamps; the issue-fix gate and repo-hygiene's docker leg — the previously unnamed third caller — invoke the contracts script without the variable and accept the 5s default. The alternative (exporting AUTOFIX_VITEST_FLAGS in the issue-fix gate step) edits a workflow file this PR has never touched and stays out of scope. R2-2 --maxWorkers=25% is coerced to NaN by vitest 1.x; the lockfile-pinned 1.6.1 under packages/sdk-typescript survives only because its config sets a numeric poolOptions.threads.maxThreads, which tinypool reads before ctx.config.maxWorkers. Pin the shield: a new case derives vitest-1.x workspaces from nested lockfile copies and asserts each keeps the threads pool and a numeric maxThreads, failing with a directive if such a workspace is missing from the config registry. Mutation-verified red on shield removal. R2-3 The export is the only line carrying the clamps across the process boundary into check-autofix-contracts.sh; nothing pinned it. Added the structural pin plus an ordering assertion against the contracts call — deleting the export or moving it below the call now fails the suite. Both mutants verified red. R2-4 The contracts case's fake npm logged $*-joined argv, rendering a joined-blob flag byte-identically to separate words; the [*]-for-[@] mutant survived. The shim now logs one bracketed line per argv word and the four expectations in the case were updated; the mutant now fails. R1-3/R1-4/R1-5/R1-6 (round-2 commit) re-verified with mutation probes: dropping either invocation's expansion, dropping the assignment, emptying the array, drifting --testTimeout to 61000, and dropping the flag expansion inside the contracts script each turn an existing witness red. R1-1 remains declined: the deterministic crash does not reproduce at this head (the leg passes with sdk's shield present), but the round-2 rationale was wrong and is corrected on the thread; the residual risk is the shield R2-2 now pins. * test(autofix): pin both sides of the gate's AUTOFIX_VITEST_FLAGS transport Addresses review round 3 on QwenLM#10671; both pins mutation-verified. R3-1 Nothing pinned the VITEST_LOAD_CLAMPS definition above its consumers: the existing pins are position-blind (toContain here, the parity regex in unit-vitest-configs.test.ts matches anywhere), so moving the array below its consumers left every pin green while bash expanded the then-unset array to zero words under the gate's `set -eo pipefail` without `-u` — AUTOFIX_VITEST_FLAGS goes empty and the package and bite legs lose all four clamps, silently reverting to the incident conditions. Added an explicit ordering pin against the star-join, the first consumer in script order, which pins the definition above every consumer. Outright deletion was already caught by the parity test's existence assertion; the move was the only surviving hole. Move mutant verified red (61668 < 34052 fails). R3-2 The remove side was pinned nowhere: moving `unset AUTOFIX_VITEST_FLAGS` above the contracts call (or deleting it) strips the export the drift leg inherits at child-spawn time, leaving the web-shell drift test at vitest's 5s default with every establish-side pin green. Added the symmetric ordering pin, contracts call before unset. Move and delete mutants both verified red (34333 < 34241 and 34306 < -1 fail). --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: 易良 <1204183885@qq.com>
|
Released in v0.23.0. |


What this PR does
Adds a
propose_goalcore tool: the model proposes a session Goal, the user sees the full objective in an approval dialog, and only their approval sets it — exactly as/goal setwould. Declining sets nothing; the model is told only that the Goal was not set and must not propose it again. The tool is registered besideget_goal/update_goal(so never for subagents), only in interactive terminal sessions (resolveInteractionMode === 'interactive'— never headless, and not yet in Web Shell or other ACP-driven sessions, which bypass the turn boundary that applies the approval) and only while the newgoals.modelProposedsetting is not"disabled".The dialog reuses the generic
infoconfirmation, so no host needs a new component: the objective is in the invocation description (the one field every host forwards — the Web Shell does not render aninfoprompt) and in the plain-text prompt together with a one-sentence explanation of what approving does. The invocation declaresrequiresUserInteraction(), so nopermissions.allowrule, no skillallowedToolsgrant, and no approval mode — YOLO included, and AUTO_EDIT, which auto-approvesinfoconfirmations — can skip the dialog. Preconditions are checked before the dialog and again inexecute(), because/goalcan change the session while the dialog is open: plan mode, an untrusted folder, a session without Goal persistence, and an active Goal all refuse with guidance. An active Goal is never replaced from the tool (the model is told to hand over a/goal edit …//goal set …line instead); a stopped Goal (paused, blocked, complete, usage-limited) is replaced throughreplacewith its expected version, so a Goal that changed under the dialog is rejected rather than clobbered. The tool never dispatches from inside the turn: creating the Goal mid-turn would leave the proposing turn's remaining tool-result continuations without a Goal permit (the first end-to-end run surfaced exactly that as anAn active Goal requires an exact turn permiterror card). The approval is parked onConfig(a set-once slot) andGeminiClient.sendMessageStreamapplies it at the turn's true terminal sites — after queued steer input, blocking Stop-hook continuations, and the next-speaker recursion have completed — asking the runtime to refuse if a Goal became active meanwhile; the runtime's broadcast then renders the Goal card and starts the first Goal turn right after the model's one-sentence acknowledgement. The approval is bound to the proposing turn's prompt id, so only that turn's own terminal boundary can apply it and any other frame — a Notification, Cron, or Teammate turn, the user's next query, a hook-blocked or recursive frame — drops it on sight; an abort drops it too (and pauses a Goal an abort raced past), and every early-exit path discards it as a fast path.goals.modelProposed("alwaysAsk"default,"disabled") is added to the settings schema and toWORKSPACE_RESTRICTED_SETTINGS, so a repository's.qwen/settings.jsoncannot switch the tool on; the CLI normalizes the value to the closed enum before it reaches core. The bundledgoal-draftskill now hands off throughpropose_goalwhen it is available and no Goal is active, and keeps its printed/goal setline for headless runs, the disabled setting, and the active-Goal case. Docs (goals.md,settings.md, the design note) and the Web Shell tool-name tables are updated.Why it's needed
#10002 shipped
/goal-draft, which ends by printing a/goal set …line for the user to copy and run — because built-in commands are not model-invocable, and deliberately so: starting an unattended loop is the user's decision. That hand-off works but is clumsy (a 1,000-character line to paste, and on some terminals the backticks get escaped), and it is the one place where the model's careful draft can be lost. Claude Code solves this withProposeGoal+ amodelProposedGoalssetting: the model proposes, the user approves with one keypress. This PR is the qwen-code equivalent, with two deliberate differences: there is noautomode that lets the model skip the dialog, and the tool never replaces a running Goal.Reviewer Test Plan
How to verify
cd packages/core && npx vitest run src/goals src/core/client-goal.test.ts src/config/config.test.ts src/skills/bundled/goal-draft/SKILL.test.tsandcd packages/cli && npx vitest run src/config/settings.test.ts src/config/config.test.ts— all pass. New cases:ProposeGoalTool(dialog contents, approval parks the proposal andapplyPendingGoalProposalthen sets the Goal and the runtime starts the first Goal turn, cancel parks nothing, refuses in plan mode / untrusted / no persistence / active Goal, replaces a paused Goal, refuses over a Goal that became active meanwhile, reports a moved version as a conflict, validation), the client boundary (a parked approval is applied once a user turn ends without tool calls; one still parked at the next user query is discarded), registration (present in interactive/ACP/stream-json, absent headless and when disabled),goals.modelProposedscope (honored from user scope, stripped from workspace scope with a warning), and the CLI normalizer.testscript:/goal-draft make the auth tests pass and raise coverage. Expected: after drafting, a confirmation cardPropose Goal: Outcome: …with the objective and "Set this as the session Goal? Approving sets it like /goal set …", options "Yes, allow once" / "No" only (no "Always allow"). Enter → Goal cardcreate, the footer Goal pill appears, and the first Goal turn starts on its own;/goalshows revision 1 with the proposed objective./goalreports none, and the model does not re-propose.-y) interactive: the dialog still appears. Plan mode: the tool returns "Keep planning; propose the Goal after the plan is approved." With a Goal active: the model prints a/goal edit …line instead.qwen -p "/goal-draft …"(headless): unchanged —propose_goalis not registered and the/goal setline is printed."goals": {"modelProposed": "disabled"}in~/.qwen/settings.json: the tool disappears (the skill falls back to the printed line). Put the same in.qwen/settings.jsonof a project: a startup warning says the value is ignored.Evidence (Before & After)
TUI in YOLO mode (this machine's default), trusted throwaway project with
test/coveragescripts, driven in tmux (160×48) against DashScopeqwen3.8-max; screenshots are the tmux pane rendered with its own colors.Before (
main+ #10002):/goal-draftends with a/goal set …line to copy; there is nopropose_goaltool.After — 1. The skill's clarifying questions still come as a real dialog in YOLO (that is #10160), and the answers feed the draft.
After — 2.
propose_goalrenders the objective and the one-sentence explanation in the approval dialog. Only "Yes, allow once" / "No" are offered — no "Always allow", even in YOLO.After — 3. Enter → the tool card reports
Goal approved ·, the model acknowledges in one sentence and stops, and at that boundary the runtime sets the Goal: theGoal activecard appears and the footer pill reads/goal active. NoAn active Goal requires an exact turn permiterror card (the first end-to-end run, which set the Goal from inside the tool, produced one right here — hence the boundary application).After — 4. The runtime drives the first Goal turn on its own (here: diagnosing the failing
node --test test/invocation) while the pill counts up.On the first end-to-end run the same loop ran to the end: the runtime installed c8, added the missing-branch tests, pasted both check lines, and the independent verifier accepted the completion (
Goal complete · 1 turn · 3m 57s). Cancelling the dialog with Esc leaves/goalempty (unit-tested; the scheduler cancels the call beforeexecute, andexecuteitself parks nothing without approval).Tested on
Environment (optional)
Linux, Node 22,
npm ci+npm run build+npm run bundle; TUI driven in tmux (160×50) against DashScopeqwen3.8-max.Risk & Scope
infoconfirmation, so its buttons read "Yes, allow once" / "No, suggest changes" in the TUI and "Allow" / "Reject" in the Web Shell and ACP clients rather than "Set this goal" / "Not now"; the prompt text explains what approving does. A dedicated confirmation type would need rendering in all three hosts and is left for later. The tool never replaces an active Goal — replacing mid-run would preempt the running Goal turn from inside one of its own tool calls.infopath and unit tests, not by a new screenshot;parseGoalCommandstill flattens newlines (separate follow-up); noautomode.Linked Issues
Follow-up to #10002 (P2 of the goal-draft design note).
中文说明
这个 PR 做了什么
新增核心工具
propose_goal:模型提议一个会话 Goal,用户在审批对话框里看到完整 objective,只有用户批准才会设定——效果与/goal set完全一致。拒绝则什么都不设;模型只被告知"Goal 未设定",且不得再次提议。该工具与get_goal/update_goal一起注册(因此子 agent 永远没有),仅在交互式终端会话(resolveInteractionMode === 'interactive'——headless 没有,Web Shell 及其他 ACP 驱动的会话也暂时没有,它们不经过应用提案的 turn 边界)、且新设置goals.modelProposed不为"disabled"时才注册。对话框复用通用的
info确认类型,三端都不需要新组件:objective 同时放在调用描述里(这是每个宿主都会转发的唯一字段——Web Shell 不渲染info的 prompt)和纯文本 prompt 里,并附一句"批准意味着什么"的说明。调用声明了requiresUserInteraction(),所以任何permissions.allow规则、skill 的allowedTools授权、任何审批模式——包括 YOLO,以及会自动批准info确认的 AUTO_EDIT——都跳不过对话框。前置条件在弹窗前检查一次、execute()里再检查一次,因为对话框打开期间/goal可能改变会话:plan 模式、不受信任的目录、没有 Goal 持久化的会话、以及已有活跃 Goal,都会带指引拒绝。活跃 Goal 永远不会被这个工具替换(模型被要求改为给出/goal edit …//goal set …行);已停止的 Goal(paused、blocked、complete、usage-limited)通过带期望版本的replace替换,所以在对话框下被改动过的 Goal 会被拒绝而不是被覆盖。工具从不在 turn 内部 dispatch:在 turn 中途创建 Goal 会让本 turn 剩余的工具结果续跑失去 Goal permit(第一次端到端运行正是暴露了An active Goal requires an exact turn permit这个错误卡片)。批准被暂存在Config上(set-once 槽),由GeminiClient.sendMessageStream在 turn 真正的终止点(排队的 steer、阻塞 Stop hook 续跑、next-speaker 递归都结束之后)应用,并要求 runtime 在期间出现活跃 Goal 时拒绝;随后 runtime 的广播渲染 Goal 卡片,并在模型的一句话确认之后紧接着启动首个 Goal turn。批准绑定提议 turn 的 prompt id:只有该 turn 自己的终止点能应用它,任何其它 frame——Notification/Cron/Teammate turn、用户的下一条消息、被 hook 拦截或递归的 frame——一旦碰到就丢弃;中止同样丢弃(若与 dispatch 竞争则暂停新 Goal),各提前退出路径的丢弃保留为快路径。goals.modelProposed(默认"alwaysAsk",可选"disabled")加入设置 schema 和WORKSPACE_RESTRICTED_SETTINGS,仓库的.qwen/settings.json无法打开这个工具;CLI 会先把值规范到封闭枚举再传给 core。内置goal-draftskill 现在在工具可用且没有活跃 Goal 时通过propose_goal交接,在 headless、设置禁用、或已有活跃 Goal 时保留打印/goal set行的方式。文档(goals.md、settings.md、设计说明)和 Web Shell 的工具名表已更新。为什么需要
#10002 交付的
/goal-draft以打印一行/goal set …让用户复制运行收尾——因为内置命令不可被模型调用,而且这是刻意的:启动无人值守循环应由用户决定。这个交接能用但笨拙(一行 1000 字符要粘贴,某些终端还会转义反引号),也是模型精心起草的内容最容易丢失的地方。Claude Code 用ProposeGoal+modelProposedGoals设置解决:模型提议,用户一键批准。本 PR 是 qwen-code 的对应实现,有两处刻意的差异:没有让模型跳过对话框的auto模式;工具永远不替换运行中的 Goal。Reviewer 测试计划
如何验证
cd packages/core && npx vitest run src/goals src/core/client-goal.test.ts src/config/config.test.ts src/skills/bundled/goal-draft/SKILL.test.ts和cd packages/cli && npx vitest run src/config/settings.test.ts src/config/config.test.ts——全部通过。新增用例:ProposeGoalTool(对话框内容、批准后暂存提案且applyPendingGoalProposal随后设定 Goal 并由 runtime 启动首个 Goal turn、取消不暂存、plan 模式 / 未信任 / 无持久化 / 活跃 Goal 时拒绝、替换 paused Goal、期间变为活跃则拒绝、版本移动报冲突、参数校验),client 边界(用户 turn 无工具调用结束时应用暂存的批准;下一条用户消息开始时仍暂存的被丢弃),注册(交互/ACP/stream-json 存在,headless 与禁用时不存在),goals.modelProposed作用域(user 层生效,workspace 层剥离并告警),以及 CLI 的规范化函数。test脚本的项目:/goal-draft make the auth tests pass and raise coverage。预期:起草后出现确认卡片Propose Goal: Outcome: …,带 objective 和"Set this as the session Goal? Approving sets it like /goal set …",选项只有 "Yes, allow once" / "No"(没有 "Always allow")。回车 → Goal 卡片create、底部 Goal pill 出现、首个 Goal turn 自动开始;/goal显示 revision 1 与提议的 objective。/goal报告无,模型不会再提议。-y)交互:对话框仍然出现。Plan 模式:工具返回 "Keep planning; propose the Goal after the plan is approved."。已有活跃 Goal:模型改为打印/goal edit …行。qwen -p "/goal-draft …"(headless):不变——propose_goal未注册,仍打印/goal set行。~/.qwen/settings.json写"goals": {"modelProposed": "disabled"}:工具消失(skill 回退到打印行)。在项目.qwen/settings.json写同样内容:启动时告警该值被忽略。证据(Before & After)
Before:
/goal-draft以一行待复制的/goal set …结束;没有propose_goal工具。After:见上方英文部分的 5 张 tmux 截图——skill 提问对话框(YOLO 下仍弹出)、
propose_goal审批对话框(只有 Yes/No,无 Always allow)、批准后Goal approved ·→ 模型一句话确认 → 边界处Goal active卡片与底部/goal activepill、以及 runtime 自行驱动首个 Goal turn;没有再出现An active Goal requires an exact turn permit错误卡片。首次端到端运行中同一循环跑到了Goal complete · 1 turn · 3m 57s。测试平台
Linux ✅;macOS、Windows⚠️ 未测试。
环境(可选)
Linux,Node 22,
npm ci+npm run build+npm run bundle;TUI 在 tmux(160×50)中针对 DashScopeqwen3.8-max驱动。风险与范围
info确认,TUI 里按钮文案是 "Yes, allow once" / "No, suggest changes",Web Shell 与 ACP 客户端里是 "Allow" / "Reject",而不是 "Set this goal" / "Not now";prompt 文本解释了批准的含义。专用确认类型需要三端各自渲染,留待后续。工具永远不替换活跃 Goal——运行中替换会从 Goal 自己的工具调用内部抢占正在运行的 Goal turn。info路径和单元测试覆盖,没有新截图;parseGoalCommand仍会压平换行(另行跟进);没有auto模式。关联 Issue
#10002 的后续(goal-draft 设计说明的 P2)。